package com.gitee.ixtf.vertx.verticle

import cn.hutool.core.net.url.UrlQuery
import cn.hutool.core.util.StrUtil
import com.gitee.ixtf.core.J
import com.gitee.ixtf.core.kotlinx.IxtfEnvInt
import com.gitee.ixtf.core.kotlinx.IxtfEnvString
import com.gitee.ixtf.guice.Jguice
import com.gitee.ixtf.guice.kotlinx.get
import com.gitee.ixtf.vertx.handler.FailureHandler
import com.gitee.ixtf.vertx.internal.RestfulMethod
import graphql.GraphQL
import io.netty.handler.codec.compression.StandardCompressionOptions
import io.vertx.core.Future
import io.vertx.core.Handler
import io.vertx.core.http.HttpServer
import io.vertx.core.http.HttpServerOptions
import io.vertx.ext.auth.authentication.AuthenticationProvider
import io.vertx.ext.auth.authentication.Credentials
import io.vertx.ext.auth.authentication.TokenCredentials
import io.vertx.ext.auth.authentication.UsernamePasswordCredentials
import io.vertx.ext.healthchecks.HealthCheckHandler
import io.vertx.ext.web.Router
import io.vertx.ext.web.common.WebEnvironment
import io.vertx.ext.web.handler.*
import io.vertx.ext.web.handler.graphql.GraphQLHandler
import io.vertx.ext.web.handler.graphql.GraphQLHandlerBuilder
import io.vertx.ext.web.handler.graphql.GraphiQLHandler
import io.vertx.ext.web.handler.graphql.GraphiQLHandlerBuilder
import io.vertx.ext.web.handler.graphql.ws.*
import io.vertx.kotlin.core.http.httpServerOptionsOf
import io.vertx.kotlin.core.json.json
import io.vertx.kotlin.core.json.obj
import io.vertx.kotlin.coroutines.coAwait
import io.vertx.kotlin.ext.web.handler.graphql.graphQLHandlerOptionsOf
import io.vertx.kotlin.ext.web.handler.graphql.graphiQLHandlerOptionsOf
import io.vertx.kotlin.ext.web.handler.graphql.ws.graphQLWSOptionsOf
import io.vertx.micrometer.PrometheusScrapingHandler
import jakarta.inject.Inject
import java.nio.charset.StandardCharsets.UTF_8
import java.util.*
import kotlin.jvm.optionals.getOrNull
import org.dataloader.DataLoaderRegistry
import org.dataloader.registries.ScheduledDataLoaderRegistry

abstract class BaseApiVerticle : BaseCoroutineVerticle() {
  @Inject protected lateinit var failureHandler: FailureHandler
  @Inject protected lateinit var corsHandler: CorsHandler
  @Inject protected lateinit var bodyHandler: BodyHandler
  @Inject protected lateinit var authHandlerOpt: Optional<Future<AuthenticationHandler>>
  @Inject protected lateinit var graphqlOpt: Optional<GraphQL>
  @Inject protected lateinit var restfulOpt: Optional<Collection<RestfulMethod>>
  private lateinit var httpServer: HttpServer
  protected open val service by IxtfEnvString("ixtf.api.service")
  protected open val host by IxtfEnvString("ixtf.api.host", "0.0.0.0")
  protected open val port by IxtfEnvInt("ixtf.api.port", 8080)
  protected open val graphQLWSAuth: Future<out AuthenticationProvider>? = null

  @ExperimentalStdlibApi
  override suspend fun start() {
    httpServer =
        vertx
            .createHttpServer(
                httpServerOptionsOf(compressionSupported = true).apply {
                  addWebSocketSubProtocol("graphql-transport-ws")
                  addCompressor(StandardCompressionOptions.gzip())
                  addCompressor(StandardCompressionOptions.deflate())
                  // addCompressor(StandardCompressionOptions.brotli())
                  // addCompressor(StandardCompressionOptions.zstd())
                  prepare(this)
                })
            .requestHandler(
                Router.router(vertx).apply {
                  route().failureHandler(failureHandler)
                  route().handler(corsHandler)
                  route().handler(bodyHandler)
                  route().handler(ResponseTimeHandler.create())
                  route().handler(HSTSHandler.create())
                  this["/health"].handler(HealthCheckHandler.create(vertx))
                  this["/metrics"].handler(PrometheusScrapingHandler.create())
                  val authHandler = authHandlerOpt.getOrNull()?.coAwait()
                  buildApi(authHandler)
                      .takeIf { J.nonEmpty(it.routes) }
                      ?.also { route().subRouter(it) }
                      ?.takeIf { J.nonBlank(service) }
                      ?.also { route("/api/services/$service/actions/*").subRouter(it) }
                  prepare(this, authHandler)
                })
            .listen(port, host)
            .coAwait()
  }

  override suspend fun stop() {
    httpServer.close().coAwait()
  }

  @ExperimentalStdlibApi
  private suspend fun buildApi(authHandler: AuthenticationHandler?): Router =
      Router.router(vertx).apply {
        this["/health"].handler(HealthCheckHandler.create(vertx))
        this["/metrics"].handler(PrometheusScrapingHandler.create())
        sockRoute(authHandler)?.let { route("/eventbus/*").subRouter(it) }
        graphqlOpt.ifPresent { graphql ->
          route("/graphql")
              .handler(
                  GraphQLWSHandler.builder(graphql).run {
                    prepare(this)
                    build()
                  })

          with(route("/graphql")) {
            authHandler?.let { handler(it) }
            handler(
                GraphQLHandler.builder(graphql).run {
                  prepare(this)
                  build()
                })
          }
          if (WebEnvironment.development()) {
            route("/graphiql/*").subRouter(prepare(GraphiQLHandler.builder(vertx)))
            with(get("/static/*")) {
              handler(StaticHandler.create("io/vertx/ext/web/handler/graphiql/static"))
            }
          }
        }
        restfulOpt.getOrNull()?.forEach { restful ->
          with(route("/${restful.route}")) {
            restful.consumes?.forEach { consumes(it) }
            restful.produces?.forEach { produces(it) }
            authHandler?.takeUnless { restful.permitAll }?.also { handler(it) }
            handler(restful.handler)
          }
        }
      }

  protected open suspend fun prepare(router: Router, authHandler: AuthenticationHandler?) = Unit

  protected open suspend fun prepare(options: HttpServerOptions) = Unit

  protected open suspend fun sockRoute(authHandler: AuthenticationHandler?): Router? = null

  protected open fun prepare(builder: GraphiQLHandlerBuilder): Router =
      builder.with(graphiQLHandlerOptionsOf(graphQLWSEnabled = true)).build().router()

  protected open fun prepare(builder: GraphQLHandlerBuilder) {
    builder
        .with(
            graphQLHandlerOptionsOf(requestMultipartEnabled = true, requestBatchingEnabled = true))
        .beforeExecute { ctx ->
          val dataLoaderRegistry = Jguice.get<DataLoaderRegistry>()
          ctx.context().addEndHandler {
            val scheduledDataLoaderRegistry = dataLoaderRegistry as? ScheduledDataLoaderRegistry
            scheduledDataLoaderRegistry?.close()
          }
          ctx.builder().dataLoaderRegistry(dataLoaderRegistry)
        }
  }

  protected open fun prepare(builder: GraphQLWSHandlerBuilder) {
    builder
        .with(graphQLWSOptionsOf())
        .onConnectionInit { event ->
          graphQLWSAuth
              ?.flatMap { it.authenticate(event.credentials()) }
              ?.onSuccess { event.complete(it) }
              ?.onFailure { event.fail(it) } ?: event.complete(null)
        }
        .beforeExecute { ctx ->
          val dataLoaderRegistry = Jguice.get<DataLoaderRegistry>()
          ctx.context().socket().apply {
            val cleanDataLoaderRegistryHandler =
                Handler<Void> {
                  val scheduledDataLoaderRegistry =
                      dataLoaderRegistry as? ScheduledDataLoaderRegistry
                  scheduledDataLoaderRegistry?.close()
                }
            closeHandler(cleanDataLoaderRegistryHandler)
            endHandler(cleanDataLoaderRegistryHandler)
          }
          ctx.builder().apply {
            dataLoaderRegistry(dataLoaderRegistry)
            graphQLContext { it.put(Message::class.java, ctx.context()) }
          }
        }
  }

  /** graphql subscription 授权 */
  protected open fun ConnectionInitEvent.credentials(): Credentials? =
      message().run {
        val payload =
            message()?.getJsonObject("payload")?.takeUnless { it.isEmpty }
                ?: socket()
                    .query()
                    ?.takeIf { it.isNotBlank() }
                    ?.let { UrlQuery.of(it, UTF_8).queryMap }
                    ?.let { json { obj(it.map { (k, v) -> StrUtil.trim(k) to StrUtil.trim(v) }) } }
        payload?.takeIf { it.containsKey("token") }?.let { TokenCredentials(it) }
            ?: payload
                ?.takeIf { it.containsKey("username") }
                ?.let { UsernamePasswordCredentials(it) }
      }
}
